#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Ocean Killer <ocean8192@isrc.iscas.ac.cn>
# Date: Oct 10 2020
# Description: oepkgs database maintenance tools

import json
import sys
import os
import argparse
import gnupg
import logging
import tempfile
import abc
import hashlib
import random
import logging
import subprocess
import xml.etree.ElementTree as xet

import config


class _sub_command:
    '''Base class of all sub commands
    '''
    def __init__(self, proj=None, token=None):
        self._gnupg_tmp_home = tempfile.TemporaryDirectory(
            suffix='-gnupg')

        # Due to the differ implementation of gnupg module,
        # the parameter indicates gnupg homedir is differ.
        # catch an exception to handle it
        try:
            self._gpg = gnupg.GPG(gnupghome=self._gnupg_tmp_home.name)
        except TypeError:
            self._gpg = gnupg.GPG(homedir=self._gnupg_tmp_home.name)

        self._username = None
        self._userfile = None
        self._gnupg_key = None
        self._import_res = None
        self._trust_res = None
        self._projdata = None
        self._userdata = None

        if proj:
            self.project_set(proj)

        self._token = token
        self._subcmd = ''
        self._config = config._etc_config()

        self._basepath = self._config.const.basepath

        if self._gnupg_key:
            self.key_set(self._gnupg_key)

        if not token:
            self.token_set(token)

    def _description(self, name=''):
        return f'{name} is a {name} tool'

    def project_set(self, projfile):
        self._projfile = projfile
        self._projdata = None

        with open(projfile, 'r') as f:
            self._projdata = json.load(f)

        self.user_set(self._projdata['user'])

        keyfile = self._user_key_find(self._username)
        with open(keyfile, 'r') as f:
            self._gnupg_key = f.read()
            self.key_set(self._gnupg_key)

    def key_set(self, gnupg_key):
        self._import_res = self._gpg.import_keys(gnupg_key)
        self._trust_res = self._gpg.trust_keys(
            self._import_res.fingerprints[0],
            # Python GNUPG only use this level key to encrypt
            'TRUST_ULTIMATE'
        )

        if self._token:
            self._enc_token_gen()
            self._sha1_gen()

    def dir_set(self, directory):
        self._base_path = directory

    def token_set(self, token):
        self._token = token

        if self._gnupg_key:
            self._enc_token_gen()
            self._sha1_gen()

    def user_set(self, username):
        self._username = username
        self._userfile = self._user_json_find()
        with open(self._userfile, 'r') as f:
            self._userdata = json.load(f)

    @abc.abstractmethod
    def subparser_add(self, subparsers):
        pass

    @abc.abstractmethod
    def impl(self, args):
        pass

    def _enc_token_gen(self):
        self._enc_token = self._gpg.encrypt(
            self._token,
            recipients=self._import_res.fingerprints[0]
        )

    def _sha1_gen(self):
        from hashlib import sha1

        sum = sha1()
        sum.update(bytes(
            self._token,
            encoding='ascii'
        ))

        self._sha1sum = sum.hexdigest()

    def _user_json_find(self, username):
        return os.path.join(
            self._config.const.basepath,
            'users',
            username[0:1],
            f'{username}.json'
        )

    def _user_key_find(self, username):
        jsonfile = self._user_json_find(username)
        keyid = ''
        keypath = ''

        with open(jsonfile, 'r') as f:
            data = json.load(f)
            keyid = data['user_pub']

        return os.path.join(
            self._base_path,
            'keys/user_pub',
            keyid[0:2],
            keyid
        )


class _subcmd_apitoken(_sub_command):
    '''Subcommand apitoken implementation
    '''
    def __init__(self, proj=None, token=None):
        super().__init__(proj, token)

    def subparser_add(self, subparsers):
        parser = subparsers.add_parser("apitoken",
                                       help="apitoken operations",
                                       description=(
                                           f'apitoken token'
                                           f' maintainence tool'
                                       ))

        parser.add_argument("-u", "--user",
                            required=True,
                            nargs='+',
                            help="username here")
        parser.add_argument("-t", "--token",
                            required=True,
                            nargs='+',
                            help="token string here")
        parser.add_argument("-C", "--directory",
                            nargs='+',
                            default=[os.getenv('PWD')],
                            help="working directory here")

        parser.set_defaults(func=self.impl)

    def impl(self, paras):
        if len(paras.token) != 1:
            logging.error("Incorrect number of token")
            return -1
        if len(paras.user) != 1:
            logging.error("Incorrect number of user")
            return -1

        user = paras.user[0]

        self.dir_set(paras.directory[0])
        self.token_set(paras.token[0])

        pubkey = ''
        with open(self._config.const.mypubkey, 'rb') as f:
            pubkey = f.read()

        self.key_set(pubkey)

        directory = os.path.join(f'{paras.directory[0]}',
                                 f'keys/api_token',
                                 f'{self._sha1sum[0:2]}',)
        try:
            if not os.path.exists(directory):
                os.makedirs(directory)
                with open(os.path.join(directory, self._sha1sum),
                          'wb') as f:
                    f.write(self._enc_token.data)
        except Exception as e:
            logging.error(e)
            raise e

        file_full_path = self._user_json_find(user)
        if os.path.isfile(file_full_path):
            self.__user_json_save(file_full_path)

    def __user_json_save(self, file_full_path):
        with open(file_full_path, 'w') as f:
            json.dump(
                self._projdata,
                f,
                indent=2
            )


class _subcmd_projecttoken(_sub_command):
    '''Subcommand projecttoken implementation
    '''
    def __init__(self, proj=None, token=None):
        super().__init__(proj, token)

    def subparser_add(self, subparsers):
        parser = subparsers.add_parser("projecttoken",
                                       help="projecttoken operations",
                                       description=(
                                           f'project token'
                                           f' maintainence tool'
                                       ))
        parser.add_argument("-p", "--project",
                            required=True,
                            nargs='+',
                            help="project json path")
        parser.add_argument("-t", "--token",
                            required=True,
                            nargs='+',
                            help="token string here")
        parser.add_argument("-C", "--directory",
                            nargs='+',
                            default=[os.getenv('PWD')],
                            help="config repo directory here")

        parser.set_defaults(func=self.impl)

    def impl(self, paras):
        if len(paras.token) != 1:
            logging.error("Incorrect number of token")
            return -1
        if len(paras.project) != 1:
            logging.error("Incorrect name of project")
            return -1

        token = paras.token[0]
        project = paras.project[0]
        directory = paras.directory[0]

        self.dir_set(directory)
        self.token_set(token)
        self.project_set(project)

        proj_key_path = os.path.join(
            f'{self._basepath}',
            'keys/project_token',
            self._sha1sum[0:2]
        )

        try:
            if not os.path.exists(proj_key_path):
                os.makedirs(proj_key_path)
            with open(os.path.join(proj_key_path, self._sha1sum), 'wb') as f:
                f.write(self._enc_token.data)
        except Exception as e:
            logging.error(e)
            raise e

        self._projdata['project_token'] = self._sha1sum
        self.__enc_proj_token_save()

    def __enc_proj_token_save(self):
        with open(self._projfile, 'w') as f:
            json.dump(
                self._projdata,
                f,
                indent=2
            )


class _subcmd_import(_sub_command):
    '''Subcommand to import data from json to database
    '''
    def __init__(self):
        super().__init__()

    def impl(self, paras):
        if len(paras.database) != 1:
            logging.error("Incorrect database file")
            return -1
        if len(paras.json) != 1:
            logging.error("Incorrect JSON file")
            return -1
        if len(paras.type) != 1:
            logging.error("Incorrect type")
            return -1

        self.__dbfile = paras.database[0]
        self.__jsonfile = paras.json[0]
        self.__type = paras.type[0]

        self.__db = config._data_source_sqlite3()
        self.__db.data_source_set(paras.database[0])
        self.__json = config._data_source_json()
        self.__json.data_source_set(paras.json[0])

        if self.__type == 'user':
            self.__user_import(self.__json.json_get())
        elif self.__type == 'project':
            if not self.__proj_import(self.__json.json_get()):
                return -2
        else:
            logging.error(f'Wrong type {self.__type}')
            return -1

    def __proj_import(self, jsondata):
        cur_proj = jsondata['job']
        cur_user = jsondata['user']

        # check user existance
        if not self.__db.record_exist(
                'users',
                'NAME',
                cur_user
        ):
            logging.error(
                f'user {cur_user} is not existed, '
                'please import the user json first'
            )

            return False

        if not self.__db.record_exist(
                'projects',
                'JOB_NAME',
                f'{cur_proj}'
        ):
            # Create a record here
            # SCM_REPO, BRANCH should not be empty
            self.__db.sql_run(
                f'INSERT INTO '
                f'projects(JOB_NAME, SCM_REPO, BRANCH, WEBHOOK_URL) '
                f'values("{cur_proj}", "a", "b", "c")'
            )

        def _db_set_by_job_name(_k, _v):
            # dict for json item to database column
            json_to_db = self._config.const.J2D['projects']

            self.__db.value_set('projects', json_to_db[_k], f'{_v}',
                                'JOB_NAME', f'"{cur_proj}"')

        val_dict = jsondata
        val_dict['user'] = int(
            self.__db.value_get(['ID', 'users'], ['NAME', f'{cur_user}'])
        )
        for key, val in val_dict.items():
            _db_set_by_job_name(key, val)

        print(f'Project {cur_proj} has imported')

        return True

    def __user_import(self, jsondata):
        cur_name = jsondata['user']
        if not self.__db.record_exist(
                'users',
                'NAME',
                f'{cur_name}'
        ):
            # REPO_PREFIX should have a non empty value
            self.__db.sql_run(
                f'INSERT INTO '
                f'users(NAME) '
                f'values("{cur_name}")'
            )

        def _db_set_by_name(_k, _v):
            # dict for json item to database column
            json_to_db = self._config.const.J2D['users']

            self.__db.value_set('users', json_to_db[_k], f'"{_v}"',
                                'NAME', f'"{cur_name}"')

        for key, val in jsondata.items():
            _db_set_by_name(key, val)

        # Set the password status need reset as default
        self.__db.value_set('users', 'PASS_NEED_RESET', 1,
                            'NAME', f'"{cur_name}"')

        print(f'User {cur_name} has imported.', flush=True)

    def __alter_row(self, key, value):
        pass

    def subparser_add(self, subparsers):
        parser = subparsers.add_parser("import",
                                       help="import data to database",
                                       description=(
                                           f'database import tool'
                                       ))

        parser.add_argument("-d", "--database",
                            required=True,
                            nargs='+',
                            help='sqlite3 database file path'
                            )
        parser.add_argument("-j", "--json",
                            required=True,
                            nargs='+',
                            help="json file path")
        parser.add_argument("-t", "--type",
                            choices=[
                                'user',
                                'project'
                            ],
                            nargs='+',
                            default=('project'),
                            help="config repo directory here")

        parser.set_defaults(func=self.impl)


class _subcmd_export(_sub_command):
    '''Subcommand to export data from database to json
    '''
    def __init__(self):
        super().__init__()

    def impl(self, paras):
        if len(paras.database) != 1:
            logging.error("Incorrect database file")
            return -1
        if len(paras.directory) != 1:
            logging.error("Incorrect destination directory")
            return -1

        self.__dbfile = paras.database[0]
        self.__destdir = paras.directory[0]

        self.__db = config._data_source_sqlite3()
        self.__db.data_source_set(paras.database[0])
        # self.__json = config._data_source_json()
        # self.__json.data_source_set(paras.json[0])

        if not self.__db.table_exist('projects'):
            logging.error(f'Table "projects" missing in database')
            return -2
        elif not self.__db.table_exist('users'):
            logging.error(f'Table "users" missing in database')
            return -3

        prj_list = self.__db.sql_run(
            f'SELECT * FROM projects'
        )
        for prj in prj_list:
            self.__proj_export(prj)

        usr_list = self.__db.sql_run(
            f'SELECT * FROM users;'
        )
        for usr in usr_list:
            self.__user_export(usr)

        print(
            f'All items in database exported to directory {self.__destdir}',
            flush=True
        )

    def __user_export(self, usr):
        destdir = os.path.join(
            f'{self.__destdir}',
            f'users',
            f'{usr[1][0:1]}'
        )
        jsonfile = '.'.join(
            [f'{usr[1]}', 'json']
        )
        data = {}
        key_list = [
            "name",
            "real_name",
            "email",
            "api_token",
            "user_pub",
        ]

        for i in range(len(key_list)):
            data[key_list[i]] = usr[i + 1]

        try:
            if not os.path.exists(destdir):
                os.makedirs(destdir)
            with open(os.path.join(destdir, jsonfile), 'w') as f:
                json.dump(data, f, indent=2)
        except Exception as e:
            logging.error(e)

    def __proj_export(self, prj):
        destdir = os.path.join(
            f'{self.__destdir}',
            f'projects',
            f'{prj[10]}',
            f'{prj[11]}',
            f'{prj[12]}',
            f'{prj[13][0:1]}',
            f'{prj[13]}'
        )
        jsonfile = '.'.join(
            [f'{prj[1]}', 'json']
        )
        prj_list = list(prj)
        prj_list[6] = self.__db.value_get(
            ["NAME", "users"],
            ["ID", f'{prj[6]}']
        )
        data = {}
        key_list = [
            "job",
            "scm_repo",
            "branch",
            "project_token",
            "yum_repo",
            'user',
            'scratch_build',
            'WebHook_URL',
            'build_tag',
            'OS',
            'version',
            'build_type',
            'package_name'
        ]

        for i in range(len(key_list)):
            data[key_list[i]] = prj_list[i + 1]

        try:
            if not os.path.exists(destdir):
                os.makedirs(destdir)
            with open(os.path.join(destdir, jsonfile), 'w') as f:
                json.dump(data, f, indent=2)
        except Exception as e:
            logging.error(e)

    def subparser_add(self, subparsers):
        parser = subparsers.add_parser("export",
                                       help="export data from database",
                                       description=(
                                           f'database export tool'
                                       ))

        parser.add_argument("-d", "--database",
                            required=True,
                            nargs='+',
                            help='sqlite3 database file path'
                            )
        parser.add_argument("-C", "--directory",
                            nargs='+',
                            default=os.getenv('PWD'),
                            help="dest directory here")

        parser.set_defaults(func=self.impl)


class _subcmd_initdb(_sub_command):
    '''Subcommand to export data from database to json
    '''
    def __init__(self):
        super().__init__()

    def impl(self, paras):
        if len(paras.database) != 1:
            logging.error("Incorrect database file")
            return -1

        self.__dbfile = paras.database[0]

        if os.path.exists(self.__dbfile):
            logging.error("Database file already exists")
            return -2

        self.__db = config._data_source_sqlite3()
        self.__db.data_source_set(paras.database[0])

        self.__db.db_init()

        print(
            f'New database file {self.__dbfile} created',
            flush=True
        )

    def subparser_add(self, subparsers):
        parser = subparsers.add_parser("initdb",
                                       help="init a empty database",
                                       description=(
                                           f'database init tool'
                                       ))

        parser.add_argument("-d", "--database",
                            required=True,
                            nargs='+',
                            help='sqlite3 database file path'
                            )

        parser.set_defaults(func=self.impl)


class _subcmd_passreset(_sub_command):
    '''Subcommand to reset specific password
    '''
    def __init__(self):
        super().__init__()

    def impl(self, paras):
        if len(paras.database) != 1:
            logging.error("Incorrect database file")
            return -1

        self.__dbfile = paras.database[0]
        self.__db = config._data_source_sqlite3()
        self.__db.data_source_set(paras.database[0])

        # Generate a 13 characters random password
        npass = ''.join(random.sample('abcdefghijklmnopqrstuvwxyz'\
                                      'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\
                                      '1234567890',
                                      13))

        # If user give a valied password, using user's
        if hasattr(paras, 'password'):
            if isinstance(paras.password, list):
                if isinstance(paras.password[0], str):
                    if len(paras.password[0]) > 0:
                        npass = paras.password[0]

        key_gen = hashlib.sha1()
        key_gen.update(npass.encode('utf-8'))
        nkey = key_gen.hexdigest()

        sql_cmd = f'UPDATE users SET'\
            f' PASS="{nkey[0:16]}",'\
            f' PASS_NEED_RESET=1'\
            f' WHERE NAME="{paras.username[0]}";'

        try:
            self.__db.sql_run(sql_cmd)
        except Exception as e:
            print('Database error')
            print(e)

            return 128

        print(
            f'Please send password: {npass} '\
            f'to user {paras.username[0]}',
            flush=True
        )

    def subparser_add(self, subparsers):
        parser = subparsers.add_parser("passreset",
                                       help="Reset specific user's password",
                                       description=(
                                           f'User password reset tool'
                                       ))

        parser.add_argument("-d", "--database",
                            required=True,
                            nargs=1,
                            help='sqlite3 database file path'
                            )

        parser.add_argument("-u", "--username",
                            required=True,
                            nargs=1,
                            help='username for password reset'
                            )

        parser.add_argument("-p", "--password",
                            required=False,
                            nargs=1,
                            help='username for password reset'
                            )

        parser.set_defaults(func=self.impl)


class _subcmd_hookset(_sub_command):
    '''Subcommand to reset specific password
    '''
    def __init__(self):
        super().__init__()

    def impl(self, paras):
        if len(paras.database) != 1:
            logging.error("Incorrect database file")
            return -1

        self.__dbfile = paras.database[0]
        self.__db = config._data_source_sqlite3()
        self.__db.data_source_set(paras.database[0])

        ntoken = paras.token[0]
        user = paras.username[0]

        xtoken = ""

        xml_temp = None
        temp_etree = None
        try:
            cmd = [
                f'java',
                f'-jar',
                f'{self._config.const.jenkins_cli_jar}',
                f'-s',
                f'{self._config.const.jenkins_url}',
                f'-webSocket',
                f'-auth',
                f'{self._config.const.jenkins_user}'\
                f':{self._config.const.jenkins_passphrase}',
                f'get-job',
                f'oepkgs_template']
            logging.debug(f'cmd: {" ".join(cmd)}')
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
                o, e = proc.communicate()
                proc.wait()

                if proc.returncode == 0:
                    xml_temp = o.decode('utf-8')
                    temp_etree = xet.fromstring(xml_temp)
                else:
                    pass
            except subprocess.CalledProcessError as e:
                # TODO: error handler should be defined
                logging.error(e)
                raise e

            et = temp_etree
            ele = et.find(
                'triggers/com.gitee.jenkins.trigger.GiteePushTrigger/'\
                'secretToken'
            )
            xtoken = ele.text
        except Exception as e:
            logging.error(e)
            raise e

        sql_cmd = f'UPDATE users SET'\
            f' TOKEN_WEB="{ntoken}",'\
            f' TOKEN_XML="{xtoken}"'\
            f' WHERE NAME="{paras.username[0]}";'

        try:
            self.__db.sql_run(sql_cmd)
        except Exception as e:
            print('Database error')
            print(e)

            return 128

        print(
            f'Webhook token of {paras.username[0]}'\
            f' is set to {ntoken}',
            flush=True
        )

    def subparser_add(self, subparsers):
        parser = subparsers.add_parser("hookset",
                                       help="Set specific user's webhook token",
                                       description=(
                                           f'webhook default token set tool'
                                       ))

        parser.add_argument("-d", "--database",
                            required=True,
                            nargs=1,
                            help='sqlite3 database file path'
                            )

        parser.add_argument("-u", "--username",
                            required=True,
                            nargs=1,
                            help='username to set'
                            )

        parser.add_argument("-t", "--token",
                            required=True,
                            nargs=1,
                            help='webhook token from web'
                            )

        parser.set_defaults(func=self.impl)


class _subparser_fact:
    '''Sub parsers factory
    '''
    def __init__(self):
        self.__subparser_list = []

    def parser_add(self, subparsers):
        apitoken = _subcmd_apitoken()
        apitoken.subparser_add(subparsers)
        self.__subparser_list.append(apitoken)

        projecttoken = _subcmd_projecttoken()
        projecttoken.subparser_add(subparsers)
        self.__subparser_list.append(projecttoken)

        sc_export = _subcmd_export()
        sc_export.subparser_add(subparsers)
        self.__subparser_list.append(sc_export)

        sc_import = _subcmd_import()
        sc_import.subparser_add(subparsers)
        self.__subparser_list.append(sc_import)

        db_init = _subcmd_initdb()
        db_init.subparser_add(subparsers)
        self.__subparser_list.append(db_init)

        passreset_init = _subcmd_passreset()
        passreset_init.subparser_add(subparsers)
        self.__subparser_list.append(passreset_init)

        hookset_init = _subcmd_hookset()
        hookset_init.subparser_add(subparsers)
        self.__subparser_list.append(hookset_init)


def main_help():
    return f'\
    Sub-command supported:\n\
    \tapitoken\n\
    \tprojecttoken\n'


def description_gen():
    return f'\
    oepkgs database and JSON data managment tools.'


def main():
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(help=main_help(),
                                       description=description_gen())
    subparser_fact = _subparser_fact()
    subparser_fact.parser_add(subparsers)
    paras = parser.parse_args()

    try:
        if hasattr(paras, 'func'):
            return paras.func(paras)
        else:
            parser.print_usage()
            logging.error('No sub-command found')

            return -3
    except Exception as e:
        parser.print_usage()
        logging.error(e)
        raise e
        return 4


if __name__ == '__main__':
    sys.exit(main())
